/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the 'License');
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {
    ArkArrayRef,
    ArkAssignStmt,
    ArkField,
    ArkInstanceFieldRef,
    ArkInvokeStmt,
    ArkMethod,
    ArkNewArrayExpr,
    ArkParameterRef,
    ArkReturnStmt,
    ArkReturnVoidStmt,
    ArkStaticInvokeExpr,
    ArrayType,
    FunctionType,
    Local,
    Stmt,
} from 'arkanalyzer/lib';
import Logger, { LOG_MODULE_TYPE } from 'arkanalyzer/lib/utils/logger';
import { BaseChecker, BaseMetaData } from '../BaseChecker';
import {
    ClassMatcher,
    FieldMatcher,
    FileMatcher,
    MatcherCallback,
    MatcherTypes,
    MethodMatcher,
} from "../../matcher/Matchers";
import { Defects, IssueReport } from "../../model/Defects";
import { Rule } from "../../model/Rule";
import { RuleListUtil } from "../../utils/common/DefectsList";
import { FieldCategory } from 'arkanalyzer/lib/core/model/ArkField';
import { NumberConstant, StringConstant, UndefinedConstant } from 'arkanalyzer/lib/core/base/Constant';

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'TypedefCheck');
type Options = [
    {
        arrayDestructuring?: boolean;
        arrowParameter?: boolean;
        memberVariableDeclaration?: boolean;
        objectDestructuring?: boolean;
        parameter?: boolean;
        propertyDeclaration?: boolean;
        variableDeclaration?: boolean;
        variableDeclarationIgnoreFunction?: boolean;
    },
];

const defaultOptions: Options = [
    {
        arrayDestructuring: false,
        arrowParameter: false,
        memberVariableDeclaration: false,
        objectDestructuring: false,
        parameter: false,
        propertyDeclaration: false,
        variableDeclaration: false,
        variableDeclarationIgnoreFunction: false,
    },
];

type MessageIds = 'errorArrayDestructuring'
    | 'errorArrowParameter'
    | 'errorMemberVariableDeclaration'
    | 'errorObjectDestructuring'
    | 'errorParameter'
    | 'errorPropertyDeclaration'
    | 'errorVariableDeclaration'
    | 'errorVariableDeclarationIgnoreFunction';

interface MessageInfo {
    errorArrayDestructuring: string;
    errorArrowParameter: string;
    errorMemberVariableDeclaration: string;
    errorObjectDestructuring: string;
    errorParameter: string;
    errorPropertyDeclaration: string;
    errorVariableDeclaration: string;
    errorVariableDeclarationIgnoreFunction: string;
}

export class TypedefCheck implements BaseChecker {
    readonly CONST_STR: string = "const";
    readonly CONST_LET: string = "let";
    readonly CONST_CONSTRUCTOR: string = "constructor";
    readonly CONST_EQUAL: string = "=";
    readonly CONST_ARROW: string = "=>";
    private defaultOptions: Options = [{ arrayDestructuring: false }];
    private messageId: MessageIds = 'errorArrayDestructuring';
    private messages: MessageInfo = {
        errorArrayDestructuring: 'Expected & to have a type annotation',
        errorArrowParameter: 'Expected & to have a type annotation',
        errorMemberVariableDeclaration: 'Expected & to have a type annotation',
        errorObjectDestructuring: 'Expected & to have a type annotation',
        errorParameter: 'Expected & to have a type annotation',
        errorPropertyDeclaration: 'Expected & to have a type annotation',
        errorVariableDeclaration: 'Expected & to have a type annotation',
        errorVariableDeclarationIgnoreFunction: 'Expected & to have a type annotation',
    };
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    public metaData: BaseMetaData = {
        severity: 2,
        ruleDocPath: 'docs/typedef-check.md',
        description: 'Expected a type annotation',
    };

    private fieldMatcher: FieldMatcher = {
        matcherType: MatcherTypes.FIELD,
    };

    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE,
    };

    private clsMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS,
    };

    private methodMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD,
        class: [this.clsMatcher],
        file: [this.fileMatcher],
    };

    public registerMatchers(): MatcherCallback[] {
        const methodMatcherCb: MatcherCallback = {
            matcher: this.methodMatcher,
            callback: this.check,
        };
        const fieldMatcherCb: MatcherCallback = {
            matcher: this.fieldMatcher,
            callback: this.check,
        };
        return [methodMatcherCb, fieldMatcherCb];
    }

    private getMessage(replaceText: string): string {
        return this.messages[this.messageId].replace('&', replaceText);
    }

    public check = (target: ArkMethod | ArkField) => {
        this.defaultOptions = this.getDefaultOption();
        if (target instanceof ArkMethod) {
            const stmts = target.getBody()?.getCfg().getStmts() ?? [];
            const code = target.getBody()?.getCfg().getDeclaringMethod().getCode();
            this.checkParameter(target);
            this.checkArrayDestructuring(target);
            this.checkObjectDestructuring(target);
            for (const stmt of stmts) {
                this.checkArrayDestructuring(stmt);
                this.checkArrowParameter(stmt, target);
                this.checkObjectDestructuring(stmt);
                this.checkParameter(stmt);
                this.checkVariableDeclaration(stmt);
            }
        } else if (target instanceof ArkField) {
            this.checkMemberVariableDeclaration(target);
            this.checkPropertyDeclaration(target);
        }

    };

    private checkVariableDeclaration(stmt: Stmt) {
        if (this.defaultOptions[0].variableDeclaration) {
            if (stmt instanceof ArkAssignStmt) {
                const leftOp = stmt.getLeftOp();
                if (leftOp instanceof ArkArrayRef && leftOp.getBase().getName().includes('%')) {

                } else {
                    const rightOp = stmt.getRightOp();
                    const originText = stmt.getOriginalText();
                    if (rightOp instanceof StringConstant || rightOp instanceof NumberConstant) {
                        const leftOp = stmt.getLeftOp();
                        if (leftOp instanceof Local && originText) {
                            const right = originText.substring(originText.indexOf(leftOp.getName()) + leftOp.getName().length).trim();
                            const verifyRight = originText.replace('\r\n', ' ');
                            if (!right?.startsWith(':')) {
                                this.messageId = 'errorVariableDeclaration';
                                const name = leftOp.getName();
                                const needAddLine = this.countOccurrences(originText.substring(0, verifyRight.indexOf(' ' + name)), '\r\n');
                                this.addIssueReport1(stmt, name, this.getMessage(name), needAddLine);
                            }
                        }
                    } else if (rightOp instanceof UndefinedConstant) {
                        const originText = stmt.getOriginalText();
                        if (!originText?.includes(":")) {
                            this.messageId = 'errorVariableDeclaration';
                            const name = originText?.substring(originText.lastIndexOf(' ') + 1) ?? '';
                            this.addIssueReport(stmt, name, this.getMessage(name));
                        }
                    } else if (rightOp instanceof Local) {
                        if (!this.defaultOptions[0].variableDeclarationIgnoreFunction) {
                            const type = rightOp.getType();
                            const leftOp = stmt.getLeftOp();
                            if (type instanceof FunctionType && leftOp instanceof Local) {
                                const name = leftOp.getName();
                                const split = originText?.split(name) ?? ['', ''];
                                const noType = split[1].trim().startsWith(this.CONST_EQUAL);
                                if (noType) {
                                    this.addIssueReport(stmt, name, this.getMessage(name));
                                }
                            }
                        }
                    }
                }

            }
        }
    }

    private checkPropertyDeclaration(field: ArkField) {
        if (this.defaultOptions[0].propertyDeclaration && field.getCategory() === FieldCategory.PROPERTY_SIGNATURE) {
            this.messageId = 'errorPropertyDeclaration';
            const name = field.getName();
            const code = field.getCode();
            const verifyCode = code.endsWith(';') ? code.substring(0, code.indexOf(';')).trim() : code;
            field.getOriginPosition;
            if (name === verifyCode) {
                this.addIssueReport(field, name, this.getMessage(name));
            } else {
                let stmts: Stmt[] = field.getInitializer();
                for (const stmt of stmts) {
                    if (stmt instanceof ArkAssignStmt) {
                        let rightOp = stmt.getRightOp();
                        let text = stmt.getOriginalText() ?? '';
                        const verify = text.substring(text.indexOf(name) + name.length).trim();
                        if (!verify.startsWith(':')) {
                            this.addIssueReport(stmt, name, this.getMessage(name));
                        }
                    }
                }
            }
        }
    }

    private checkParameter(stmt: Stmt | ArkMethod) {
        if (this.defaultOptions[0].parameter) {
            this.messageId = 'errorParameter';
            if (stmt instanceof Stmt) {
                if (stmt instanceof ArkAssignStmt && !stmt.getOriginalText()?.includes('=>')) {
                    const rightOp = stmt.getRightOp();
                    if (rightOp instanceof Local) {
                        const type = rightOp.getType();
                        if (type instanceof FunctionType) {
                            const params = type.getMethodSignature().getMethodSubSignature().getParameters();
                            for (const param of params) {
                                if (param.getType().getTypeString() === 'unknown') {
                                    this.addIssueReport(stmt, param.getName(), this.getMessage(param.getName()));
                                }
                            }
                        }
                    }
                }
            } else if (stmt instanceof ArkMethod && !stmt.getCode()?.includes('=>')) {
                const params = stmt.getBody()?.getCfg().getDeclaringMethod().getParameters() ?? [];
                for (const param of params) {
                    if (param.getType().getTypeString() === 'unknown') {
                        const name = param.getName();
                        if (name === 'ArrayBindingPattern') {
                            this.addIssueReport(stmt, '[', '');
                        } else if (name === 'ObjectBindingPattern') {
                            this.addIssueReport(stmt, '{', '');
                        } else {
                            this.addIssueReport(stmt, param.getName(), this.getMessage(param.getName()));
                        }

                    }
                }
            }

        }
    }

    /**
     * 反例：
     * const { length } = 'text';
     * const [b, c] = Math.random() ? [1, 2] : [3, 4];
     * 
     * 正例：
     * const { length }: { length: number } = 'text';
     * const [b, c]: [number, number] = Math.random() ? [1, 2] : [3, 4];
     * for (const { key, val } of [{ key: 'key', val: 1 }]) {
     * }
     * 
     * 注：目前反例中const [b, c] = Math.random() ? [1, 2] : [3, 4];在DevEco Studio的codeLinter中识别为正确，故在此也识别为正确
     * @param stmt 
     */
    private checkObjectDestructuring(stmt: Stmt | ArkMethod) {
        if (this.defaultOptions[0].objectDestructuring) {
            if (stmt instanceof Stmt) {
                if (stmt instanceof ArkAssignStmt) {
                    const rightOp = stmt.getRightOp();
                    if (rightOp instanceof ArkInstanceFieldRef) {
                        const originText = stmt.getOriginalText();
                        if (originText?.startsWith(this.CONST_STR) && originText.includes(this.CONST_EQUAL)) {
                            const index = originText.indexOf(this.CONST_EQUAL);
                            const left = originText.substring(0, index).trim();
                            const right = originText.substring(index).trim();
                            if (!left.includes(':') && left.includes('{') && left.endsWith('}')) {
                                this.messageId = 'errorObjectDestructuring';
                                const name = left.substring(5).trim()
                                this.addIssueReport(stmt, name, '')
                            }

                        }
                    } else if (rightOp instanceof ArkStaticInvokeExpr) {
                        const originText = stmt.getOriginalText();
                        if (originText?.includes(this.CONST_EQUAL)) {
                            const index = originText.indexOf(this.CONST_EQUAL);
                            const left = originText.substring(0, index).trim();
                            const list = this.parseBraces(left);
                            list?.forEach(value => {
                                const result = originText.substring(originText.indexOf(value) + value.length);
                                if (!result.trim().startsWith(":")) {
                                    this.messageId = 'errorObjectDestructuring';
                                    const needAddLine = this.countOccurrences(originText.substring(0, originText.indexOf(value)), '\n');

                                    this.addIssueReport1(stmt, value, '', needAddLine)
                                }

                            })
                        }
                    }
                }
            } else if (stmt instanceof ArkMethod) {
                const params = stmt.getSignature().getMethodSubSignature().getParameters();
                for (const param of params) {
                    const name = param.getName();
                    const type = param.getType();
                    if (name === 'ObjectBindingPattern' && type.getTypeString() === 'unknown') {
                        this.addIssueReport(stmt, '{', '')
                    }
                }
            }
        }
    }

    private parseBraces(str: string): string[] {
        const stack: number[] = [];
        const blocks: { start: number; end: number; content: string }[] = [];

        for (let i = 0; i < str.length; i++) {
            const char = str[i];
            if (char === '{') {
                stack.push(i);
            } else if (char === '}') {
                const start = stack.pop();
                if (start === undefined) continue; // 忽略未匹配的右括号
                const content = str.slice(start, i + 1);
                blocks.push({ start, end: i, content });
            }
        }

        // 按起始位置排序，确保外层块在前
        blocks.sort((a, b) => a.start - b.start);

        return blocks.map((block) => block.content);
    }

    private countOccurrences(str: string, subStr: string): number {
        // 使用正则表达式全局匹配子字符串
        const regex = new RegExp(subStr, 'g');
        const matches = str.match(regex);

        // 如果匹配到结果，返回匹配的数量；否则返回 0
        return matches ? matches.length : 0;
    }


    private checkMemberVariableDeclaration(field: ArkField) {
        if (this.defaultOptions[0].memberVariableDeclaration && field.getCategory() === FieldCategory.PROPERTY_DECLARATION) {
            this.messageId = 'errorMemberVariableDeclaration';
            const name = field.getName();
            const code = field.getCode();
            const verifyCode = code.endsWith(';') ? code.substring(0, code.indexOf(';')).trim() : code;
            field.getOriginPosition;
            if (name === verifyCode) {
                this.addIssueReport(field, name, this.getMessage(name));
            } else {
                let stmts: Stmt[] = field.getInitializer();
                for (const stmt of stmts) {
                    if (stmt instanceof ArkAssignStmt) {
                        let rightOp = stmt.getRightOp();
                        let text = stmt.getOriginalText() ?? '';
                        const verify = text.substring(text.indexOf(name) + name.length).trim();
                        if (!verify.startsWith(':')) {
                            const type = rightOp.getType();
                            if (type instanceof FunctionType) {
                                if (!this.defaultOptions[0].variableDeclarationIgnoreFunction) {
                                    this.addIssueReport(stmt, name, name.startsWith('[') ? '' : this.getMessage(name));
                                }
                            } else {
                                this.addIssueReport(stmt, name, name.startsWith('[') ? '' : this.getMessage(name));
                            }
                        }
                    }
                }
            }
        }
    }

    private checkArrayDestructuring(stmt: Stmt | ArkMethod) {
        if (this.defaultOptions[0].arrayDestructuring) {
            if (stmt instanceof Stmt) {
                if (stmt instanceof ArkAssignStmt) {
                    const rightOp = stmt.getRightOp();
                    if (rightOp instanceof ArkNewArrayExpr) {
                        const originText = stmt.getOriginalText();
                        if (originText?.startsWith(this.CONST_STR) && originText.includes(this.CONST_EQUAL)) {
                            const index = originText.indexOf(this.CONST_EQUAL);
                            const left = originText.substring(0, index).trim();
                            const right = originText.substring(index).trim();
                            if (!left.includes(':')) {
                                this.messageId = 'errorArrayDestructuring';
                                if (left.endsWith(']')) {
                                    const name = left.substring(left.lastIndexOf('['));
                                    this.addIssueReport(stmt, name, '')
                                } else {
                                    const name = left.substring(left.lastIndexOf(' ') + 1);
                                    this.addIssueReport(stmt, name, this.getMessage(name))
                                }
                            }

                        }
                    }
                }
            } else if (stmt instanceof ArkMethod) {
                const params = stmt.getSignature().getMethodSubSignature().getParameters();//.getMethodSignature().getMethodSubSignature().getParameters();
                for (const param of params) {
                    const name = param.getName();
                    const type = param.getType();
                    if (name === 'ArrayBindingPattern' && type.getTypeString() === 'unknown') {
                        this.addIssueReport(stmt, '[', '')
                    }
                }
            }
        }
    }

    private checkArrowParameter(stmt: Stmt, target: ArkMethod) {
        if (this.defaultOptions[0].arrowParameter) {
            this.messageId = 'errorArrowParameter';
            if (stmt instanceof ArkReturnStmt) {
                const params = stmt.getCfg().getDeclaringMethod().getSubSignature().getParameters();
                for (const param of params) {
                    const type = param.getType().getTypeString();
                    if (type === 'unknown') {
                        this.addIssueReport(stmt, param.getName(), this.getMessage(param.getName()), true);
                    }
                }
            } else if (stmt instanceof ArkAssignStmt) {
                const rightOp = stmt.getRightOp();
                const type = rightOp.getType();
                if (type instanceof FunctionType) {
                    const params = type.getMethodSignature().getMethodSubSignature().getParameters();
                    for (const param of params) {
                        if (param.getType().getTypeString() === 'unknown') {
                            this.addIssueReport(stmt, param.getName(), this.getMessage(param.getName()));
                        }
                    }
                }
            }
        }
    }

    public getDefaultOption(): Options {
        let option: Options;
        if (this.rule && this.rule.option) {
            option = this.rule.option as Options;
            if (option[0]) {
                if (!option[0].arrayDestructuring) {
                    option[0].arrayDestructuring = false;
                }
                if (!option[0].arrowParameter) {
                    option[0].arrowParameter = false;
                }
                if (!option[0].memberVariableDeclaration) {
                    option[0].memberVariableDeclaration = false;
                }
                if (!option[0].objectDestructuring) {
                    option[0].objectDestructuring = false;
                }
                if (!option[0].parameter) {
                    option[0].parameter = false;
                }
                if (!option[0].propertyDeclaration) {
                    option[0].propertyDeclaration = false;
                }
                if (!option[0].variableDeclaration) {
                    option[0].variableDeclaration = false;
                }
                if (!option[0].variableDeclarationIgnoreFunction) {
                    option[0].variableDeclarationIgnoreFunction = false;
                }
                return option;
            }

        }
        return [{ arrayDestructuring: false, arrowParameter: false, memberVariableDeclaration: false, objectDestructuring: false, parameter: false, propertyDeclaration: false, variableDeclarationIgnoreFunction: false }];
    }

    private addIssueReport(stmt: Stmt | ArkField | ArkMethod, text: string, description: string, userCode?: boolean) {
        let currDescription = description ? description : this.metaData.description;
        const severity = this.rule.alert ?? this.metaData.severity;
        const warnInfo = stmt instanceof ArkField ? this.getLineAndColumnForArkField(stmt, text)
            : stmt instanceof ArkMethod ? this.getLineAndColumnForArkMethod(stmt, text)
                : this.getLineAndColumn(stmt!, text, userCode);
        const fixKey = warnInfo.line + '%' + warnInfo.startCol + '%' + this.rule.ruleId;
        const mergeKey = warnInfo.filePath + '%' + fixKey + '%' + currDescription;
        const defect = new Defects(warnInfo.line ?? 1, warnInfo.startCol, warnInfo.startCol, currDescription, severity, this.rule.ruleId, warnInfo.filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }

    private addIssueReport1(stmt: Stmt, text: string, description: string, needAddLine: number) {
        let currDescription = description ? description : this.metaData.description;
        const severity = this.rule.alert ?? this.metaData.severity;
        const warnInfo = this.getLineAndColumn1(stmt!, text, needAddLine);
        const fixKey = warnInfo.line + '%' + warnInfo.startCol + '%' + this.rule.ruleId;
        const mergeKey = warnInfo.filePath + '%' + fixKey + '%' + currDescription;
        const defect = new Defects(warnInfo.line ?? 1, warnInfo.startCol, warnInfo.startCol, currDescription, severity, this.rule.ruleId, warnInfo.filePath, this.metaData.ruleDocPath, true, false, false);
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }

    private getLineAndColumnForArkMethod(method: ArkMethod, text: string) {
        const line = method.getBody()?.getCfg().getDeclaringMethod().getLine();
        const arkFile = method.getDeclaringArkClass().getDeclaringArkFile();
        if (arkFile) {
            let originText = method.getBody()?.getCfg().getDeclaringMethod().getCode() ?? '';
            let startCol = method.getBody()?.getCfg().getDeclaringMethod().getColumn() ?? 0;
            const pos = originText.indexOf(text);
            if (pos !== -1) {
                startCol += pos;
                const endCol = startCol + text.length;
                const originPath = arkFile.getFilePath();
                return { line, startCol, endCol, filePath: originPath }
            }
        } else {
            logger.debug('arkFile is null');
        }
        return { line: -1, startCol: -1, endCol: -1, filePath: '' };
    }

    private getLineAndColumnForArkField(field: ArkField, text: string) {
        const originPosition = field.getOriginPosition();
        const line = originPosition.getLineNo();
        const arkFile = field.getDeclaringArkClass().getDeclaringArkFile();//.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
        if (arkFile) {
            let originText = field.getCode() ?? '';
            let startCol = originPosition.getColNo();
            const pos = originText.indexOf(text);
            if (pos !== -1) {
                startCol += pos;
                const endCol = startCol + text.length;
                const originPath = arkFile.getFilePath();
                return { line, startCol, endCol, filePath: originPath }
            }
        } else {
            logger.debug('arkFile is null');
        }
        return { line: -1, startCol: -1, endCol: -1, filePath: '' };
    }

    private getLineAndColumn(stmt: Stmt, text: string, userCode?: boolean) {
        const originPosition = stmt.getOriginPositionInfo();
        const line = originPosition.getLineNo();
        const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
        if (arkFile) {
            let originText = stmt.getOriginalText() ?? '';
            let startCol = originPosition.getColNo();
            if (userCode) {
                const originCode = stmt.getCfg().getDeclaringMethod().getCode() ?? '';
                if (originCode.endsWith(originText)) {
                    startCol -= originCode.indexOf(originText);
                    originText = originCode;
                }
            }
            const pos = originText.indexOf(text);
            if (pos !== -1) {
                startCol += pos;
                const endCol = startCol + text.length;
                const originPath = arkFile.getFilePath();
                return { line, startCol, endCol, filePath: originPath }
            }
        } else {
            logger.debug('arkFile is null');
        }
        return { line: -1, startCol: -1, endCol: -1, filePath: '' };
    }

    private getLineAndColumn1(stmt: Stmt, text: string, needAddLine: number) {
        const originPosition = stmt.getOriginPositionInfo();
        const line = originPosition.getLineNo() + (needAddLine ?? 0);
        const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
        if (arkFile) {
            let originText = stmt.getOriginalText() ?? '';
            let startCol = originPosition.getColNo();
            let pos = originText.indexOf(text);
            if (pos === 0) {
                let verifyPos = originText.indexOf(' ' + text);
                if (verifyPos > 0) {
                    pos = verifyPos + 1;
                }
            }
            if (needAddLine > 0) {
                const right = originText.substring(0, pos);
                const verifyText = right.substring(right.lastIndexOf('\r\n') + 2);
                pos = verifyText.length;
            }
            if (pos !== -1) {
                startCol += pos;
                const endCol = startCol + text.length;
                const originPath = arkFile.getFilePath();
                return { line, startCol, endCol, filePath: originPath }
            }
        } else {
            logger.debug('arkFile is null');
        }
        return { line: -1, startCol: -1, endCol: -1, filePath: '' };
    }
}
